package fr.inserm.um915.jimpute.model;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import com.sleepycat.bind.tuple.IntegerBinding;
import com.sleepycat.bind.tuple.StringBinding;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryCursor;
import com.sleepycat.je.Transaction;

public class BimReader
	{
	private static final Logger LOG=Logger.getLogger("fr.inserm.um915.jimpute");
	private AllelesResolver allelesResolver=new AllelesResolver();
	private Model model;
	private Build srcBuild=Build.hg19;
	private Build destBuild=Build.hg19;
	private PrintStream out=System.out;
	private PrintStream err=IOUtils.createNullPrintStream();
	private boolean reverse_alleles=true;
	private boolean input_is_0_based=false;
	private boolean remove_snps_on_hap_chrom=false;
	private boolean no_shift_one_if_indel=true;
	private boolean do_debug=false;
	
	private BimReader()
		{
		
		}
	
	void close()
		{
		if(this.err!=null && this.err!=System.err)
			{
			this.err.flush();
			this.err.close();
			}
		model.close();
		}
	
	private String antiparallele(String src)
		{
		StringBuilder b=new StringBuilder(src.length());
		for(int i=src.length()-1;i>=0;--i)
			{
			switch(Character.toUpperCase(src.charAt(i)))
				{
				case 'A': b.append('T'); break;
				case 'T': b.append('A'); break;
				case 'G': b.append('C'); break;
				case 'C': b.append('G'); break;
				case '-': b.append('-'); break;
				case '/': b.append('/'); break;
				case 'N': b.append('N'); break;
				default: b.append(src.charAt(i));break;
				}
			}
		return b.toString();
		}
	
	private void print(String flagFloriane,
				String tokens[],
				Integer chromOpcode,
				int pos,
				String genMap,
				String name,
				boolean strandchanged
				)
		{
		if(do_debug && strandchanged)
			{
			this.err.println("for "+name+" printing REVCOMP("+tokens[4]+"/"+tokens[5]+")");
			} 
		boolean bad=(chromOpcode==null || pos<=0);
		for(int i=0;i< tokens.length;++i)
			{
			if(i>0) out.print("\t");
			switch(i)
				{
				case 0: out.print(bad?0:chromOpcode);break; 
				case 1: out.print(name);break; 
				case 2: out.print(genMap==null?"0":genMap);break; 
				case 3: out.print(bad?0:pos);break;
				case 4:
					{
					if(strandchanged && reverse_alleles)
						{
						out.print( antiparallele(tokens[4]));
						}
					else
						{
						out.print(tokens[4]);
						}
					break;
					}
				case 5:
					{
					if(strandchanged && reverse_alleles)
						{
						out.print( antiparallele(tokens[5]));
						}
					else
						{
						out.print(tokens[5]);
						}
					break;
					}
				default: out.print(tokens[i]);break;
				}
			}
		if(flagFloriane!=null && !flagFloriane.isEmpty())
			{
			out.print("\t"+flagFloriane);
			}
		this.out.println();
		}
	
	private boolean isATGC(String allele)
		{
		if(allele.length()!=1) return false;
		switch(Character.toUpperCase(allele.charAt(0)))
			{
			case '-': return false;
			case 'A':case 'T': case 'G': case 'C': return true;
			default: return false;
			}
		}
	
	void run(Transaction txn,BufferedReader in) throws IOException
		{
		DatabaseEntry key=new DatabaseEntry();
		DatabaseEntry data=new DatabaseEntry();
		int nLine=0;
		Pattern rsPattern=Pattern.compile("rs[0-9]+");
		Pattern tab=Pattern.compile("[\t]");
		String line;
		while((line=in.readLine())!=null)
			{
			boolean strandchanged=false;
			nLine++;
			if(nLine%10000==0)
				{
				LOG.info(line+" ("+nLine+")");
				}
			String tokens[]=tab.split(line);
			if(tokens.length<6) throw new IOException("expected 6 tokens "+line);
			Chromosome chromosome=Chromosome.parse(tokens[0]);
			
			String name=tokens[1];
			
			if(chromosome==null)
				{
				this.err.println("Cannot find chromosome in "+line);
				print(null,tokens,null,-1,"0",name,false);
				continue;
				}
			
			
			int rsId=-1;
			/* is it a rs## ? */
			if(rsPattern.matcher(name).matches())
				{
				rsId=Integer.parseInt(name.substring(2));
				}
			
			String flag_floriane=null;
			ChromPosition position=null;
			for(;;)
				{
				
				Marker m=model.findMarkerByName(txn, name);
				/* ok we found it */
				if(m!=null)
					{
					if(do_debug) this.err.println("OK found "+name+" in impute data. Current allele are "+
						tokens[4]+"/"+tokens[5] +" while impute data for "+name+" are "+m.getA0()+"/"+m.getA1()
						);
						
					AllelesResolver.Resolved resolved = this.allelesResolver.resolve(tokens[4], tokens[5], m.getA0(), m.getA1());
					
					if(resolved.getMessage()!=null && !resolved.getMessage().isEmpty())
						{
						this.err.println(name+" "+resolved.getMessage());
						}
					if(resolved.getFlagEndOfLine()!=null && !resolved.getFlagEndOfLine().isEmpty())
						{
						flag_floriane=(flag_floriane==null?"":flag_floriane+":")+resolved.getFlagEndOfLine();
						}
					if(resolved.isChromZero())
						{
						m=null;
						position=null;
						break;
						}
					
					tokens[4]=resolved.getA0();
					tokens[5]=resolved.getA1();
					if(do_debug) this.err.println("Allele Resolver says alleles for "+name+" are now "+tokens[4]+"/"+tokens[5]);
					
					position=m.getPosition();
					break;
					}
				
				//check name has changed
				if(rsId!=-1)
					{
					if(do_debug) this.err.println("Cannot find in "+name+" in impute. Now scanning rsMerge");
					RsMergeArch merge=model.findRsMergeByName(txn, rsId);
					if(merge!=null)
						{
						if(do_debug) this.err.println("Found a rsmerge for "+name+" :"+merge);
						this.err.println("replace "+line+" by "+merge);
						if(merge.getOrien2Current()==1)
							{
							if(do_debug) this.err.println("orientation has changed. I will use REVCOMP at the end");
							tokens[4]=antiparallele(tokens[4]);
							tokens[5]=antiparallele(tokens[5]);					
							//NE PAS MODIFIER strandchanged, va être utilisé par alleleResolver
							flag_floriane=(flag_floriane==null?"":flag_floriane+":")+"RSMERGE_"+name+"_INVERSE_ORIENT";
							}
						rsId=merge.getRsCurrent();
						name="rs"+merge.getRsCurrent();
						if(do_debug) this.err.println("name has changed it is now "+name);
						continue;
						}
					else
						{
						if(do_debug) this.err.println("No rsMerge found for "+name);
						}
					
					
					List<BuildChromPosition> positions=this.model.findRsPositionsByName(txn, this.destBuild, rsId);
					if(positions!=null && remove_snps_on_hap_chrom && positions.size()>1)
						{
						Iterator<BuildChromPosition> iter=positions.iterator();
						while(iter.hasNext())
							{
							BuildChromPosition bcp=iter.next();
							if(Chromosome.values()[bcp.getChromosomeIdx()].name().contains("hap"))
								{
								this.err.println(name+" : ignore "+bcp);
								iter.remove();
								}
							}
						}
					
					
					if(positions==null)
						{
						this.err.println("no position for "+name+" on dbsnp "+this.destBuild);
						}
					else if(positions.size()!=1)
						{
						this.err.println("multiples positions for "+name+" on dbsnp "+this.destBuild);
						}
					else
						{
						position=new ChromPosition(
								positions.get(0).getChromosomeIdx(),
								positions.get(0).getPosition()+1 //+1 impute data are +1 based
								);
						break;
						}
					}
				else /* not a rs## , try to find one by its position */
					{
					try
						{
						int shift=(input_is_0_based?0:1);
						if(this.no_shift_one_if_indel && 
							(!isATGC(tokens[4]) || !isATGC(tokens[5]))
							)
							{
							shift=0;
							}
						BuildChromPosition bcp=new BuildChromPosition(
								(byte)srcBuild.ordinal(),
								(byte)chromosome.ordinal(),
								Integer.parseInt(tokens[3])-shift
								);
						
						Set<Integer> set=model.findRsIdByPosition(txn,
								bcp
								);
						
						
						if(set==null || set.isEmpty())
							{
							this.err.println("cannot find a rs at this position: "+line);
							}
						else if(set.size()>1)
							{
							this.err.println("multiple rs at this position: "+line+" = "+set);
							}
						else
							{
							rsId=set.iterator().next();
							
							this.err.println("mapped "+name+" to rs"+rsId+" using positions. shift="+shift);
							name="rs"+rsId;
							continue;
							}
						}
					catch(NumberFormatException err)
						{
						err.printStackTrace(this.err);
						position=null;
						}
					}
				break;
				}
			if(position==null) 
				{
				this.err.println("Cannot map "+name);
				print(null,tokens,null,-1,"0",name,false);
				continue;
				}
			
			StringBinding.stringToEntry(name, key);
			int countOcc=0;
			if(model.getExtraDatabase(txn).get(txn, key, data, LockMode.DEFAULT)==OperationStatus.SUCCESS)
				{
				countOcc=IntegerBinding.entryToInt(data);
				this.err.println("\"Y'en a un qui a été supprimé parce que l'autre a le même nom\" (Mylene Farmer) : "+name);
				print(null,tokens,null,-1,"0",name+"_"+countOcc,false);
				continue;
				}
			IntegerBinding.intToEntry(countOcc +1,data);
			if(model.getExtraDatabase(txn).put(txn, key, data)!=OperationStatus.SUCCESS)
				{
				throw new IOException("BerkeleyDB error");
				}
			
			if(position.getChromosomeIdx()!=chromosome.ordinal())
				{
				err.println("chromosome changed: "+position+" "+line);
				}
			Integer chromOpcode=Chromosome.values()[position.getChromosomeIdx()].toPlinkOpcode();
			if(chromOpcode==null)
				{
				err.println("no convert to plink: "+position+" "+line);
				}
			Combined c=model.getCombinedByPosition(txn,position );
			String genMap="0";
			if(c!=null) genMap=String.valueOf(c.getGeneticMap());
			print(flag_floriane,tokens,chromOpcode,position.getPosition(),genMap,name,strandchanged);
			}
		}
	
	public static void main(String[] args) throws Exception
		{
		File fileOut=null;
		LOG.setLevel(Level.OFF);
		BimReader app=new BimReader();
		File dbHome=null;
		int optind=0;
		while(optind<args.length)
				{
				if(args[optind].equals("-h"))
					{
					System.out.println("-d (berkeleydb home directory)");
					System.out.println("-L (log level) default:OFF");
					System.out.println("-m (message-filename)");
					System.out.println("-o (fileout) default:stdout");
					System.out.println("-r disable reverse allele if rs/strand have changed.");
					System.out.println("-s (src-build) default:"+app.srcBuild);
					System.out.println("-t (dest-build) default:"+app.destBuild);
					System.out.println("-0 if input is +0 based default:1 based");
					System.out.println("-H remove chrom_hap cromosomes");
					return;
					}
				else if(args[optind].equals("-r"))
					{
					app.reverse_alleles=false;
					}
				else if(args[optind].equals("-s") & optind+1< args.length)
					{
					app.srcBuild=Build.valueOf(args[++optind]);
					}
				else if(args[optind].equals("-t") & optind+1< args.length)
					{
					app.destBuild=Build.valueOf(args[++optind]);
					}
				else if(args[optind].equals("-d") & optind+1< args.length)
					{
					dbHome=new File(args[++optind]);
					}
				else if(args[optind].equals("-L") & optind+1< args.length)
					{
					LOG.setLevel(Level.parse(args[++optind]));
					}
				else if(args[optind].equals("-0") )
					{
					app.input_is_0_based=true;
					}
				else if(args[optind].equals("-H") )
					{
					app.remove_snps_on_hap_chrom=true;
					}
				else if(args[optind].equals("-o") & optind+1< args.length)
					{
					fileOut=new File(args[++optind]);
					}
				else if(args[optind].equals("-m") & optind+1< args.length)
					{
					String m=args[++optind];
					if(m.equals("-") || m.isEmpty())
						{
						app.err=System.err;
						}
					else
						{
						app.err=IOUtils.openWriter(new File(m));
						}
					}
				else if(args[optind].equals("--"))
					{
					optind++;
					break;
					}
				else if(args[optind].startsWith("-"))
					{
					System.err.println("Unnown option: "+args[optind]);
					return;
					}
				else
					{
					break;
					}
				++optind;
				}
		
		if(dbHome==null)
			{
			System.err.println("Undefined dbHome");
			return ;
			}
		
		
		
		
		Transaction txn=null;
		ModelFactory factory=ModelFactory.newInstance();
		factory.setReadOnly(true);
		factory.setDbHome(dbHome);
		//factory.setIgnoreChecksumRead(true);
		app.model=factory.open();
		
		if(fileOut!=null)
			{
			app.out=IOUtils.openWriter(fileOut);
			}
		
		if(optind==args.length)
			{
			LOG.info("Reading from stdin");
			app.run(txn,new BufferedReader(new InputStreamReader(System.in)));
			}
		else if(optind+1==args.length)
			{
			LOG.info("Reading from file "+args[optind]);
			BufferedReader in=IOUtils.openUrl(args[optind]);
			app.run(txn,in);
			in.close();
			}
		else
			{
			System.err.println("Illegal number of arguments");
			}
		if(fileOut!=null)
			{
			app.out.flush();
			app.out.close();
			}
		app.close();
		
		LOG.info("Done.");
		}
}
